/**
*    Copyright (c) 2011-2014, OpenIoT
*   
*    This file is part of OpenIoT.
*
*    OpenIoT is free software: you can redistribute it and/or modify
*    it under the terms of the GNU Lesser General Public License as published by
*    the Free Software Foundation, version 3 of the License.
*
*    OpenIoT is distributed in the hope that it will be useful,
*    but WITHOUT ANY WARRANTY; without even the implied warranty of
*    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
*    GNU Lesser General Public License for more details.
*
*    You should have received a copy of the GNU Lesser General Public License
*    along with OpenIoT.  If not, see <http://www.gnu.org/licenses/>.
*
*     Contact: OpenIoT mailto: info@openiot.eu
 * @author Timotee Maret
 * @author Sofiane Sarni
*/

package org.openiot.gsn.processor;

import groovy.lang.*;
import org.openiot.gsn.beans.DataField;
import org.openiot.gsn.beans.StreamElement;
import org.openiot.gsn.vsensor.AbstractVirtualSensor;
import org.apache.log4j.Logger;

import java.io.Serializable;
import java.util.Timer;
import java.util.TimerTask;
import java.util.TreeMap;

/**
 * This Processor (processing class) executes a scriptlet upon reception of a new  StreamElement and can be used to
 * implement arbitrary complex processing class by specifying its logic directly in the virtual sensor description file.
 * This is especially useful for setting up flexible, complex and DBMS independent calibration functions.
 * The current implementation supports the Groovy scripting language @see http://groovy.codehaus.org .
 * <p/>
 * Data Binding
 * ------------
 * The current implementation automatically binds the data between the StreamElement and the variables of the scriptlet.
 * The binding is based on the mapping between the StreamElement data field names, and the scriptlet variable names.
 * <p/>
 * Before executing the scriptlet, all the fields from the StreamElement received are binded to the scriptlet.
 * The scriptlet is then executed and could use both variables hard-coded or dynamically binded from the StreamElement.
 * Once the scriptlet execution is done, a new StreamElement matching the output sctructure defined in the virtual
 * sensor description file is created. The data of this StreamElement are binded from all the variables binded to the
 * scriptlet. If a field name exists in the output sctructure and no variable match it in the scriptlet, then its value
 * is set to null.
 * <p/>
 * State
 * -----
 * <p/>
 * In order to save the state of a variable for the next evaluation, the following code is automatically added to your
 * script:
 * <p/>
 * def isdef(var) {
 * (binding.getVariables().containsKey(var))
 * }
 * <p/>
 * It can be used for initializing or updating a variable like below:
 * <p/>
 * statefulCounter = isdef('statefulCounter') ? statefulCounter + 1 : 0;
 * <p/>
 * <p/>
 * PREDEFINED VARIABLES
 * --------------------
 * <p/>
 * The following variable is accessible directly in the scriptlet:
 * <p/>
 * 1. groovy.lang.Binding binding                This contains the variables binded to your scriptlet.
 * <p/>
 * PROCESSING CLASS INIT-PARAMETERS
 * --------------------------------
 * <ul>
 *      <li>
 *      scriptlet, String, mandatory if scriptlet-periodic is not specified, optional otherwise<br/>
 *      Contains the content of your script executed upon reception of a new StreamElement.
 *      </li>
 *      <li>
 *      persistant, boolean, optional<br/>
 *      Sets wether or not a StreamElement is created and stored at the end of the scriplet execution.
 *      </li>
 *      <li>
 *      scriplet-periodic, String, mandatory if scriptlet is not specified, optional otherwise<br/>
 *      Contains the content of your script which is executed periodically at 'period' ms interval.
 *      </li>
 *      <li>
 *      period, long, mandatoryif scriptlet-periodic is specified<br/>
 *      Define the period (in ms) between two execution of the scriptlet-periodic script.
 *      </li>
 * </ul>
 * PERIODICAL EXECUTION
 * --------------------
 * <p>
 * If some part of your script has to be executed at a periodical interval (and not upon reception of a new StreamElement),
 * you must set this part of code in the 'scriptlet-periodic' parameter and set the interval in the 'period' parameter.
 * Note that the 'script' and 'scriplet-periodic' scripts are never executed concurrently and that the  state is shared
 * among the two.
 * Note that the StreamElement generated by the execution of the 'scriptlet-periodic' is NOT stored in the db, even if
 * the 'persistant' parameter is enabled.
 * </p>
 * PREDEFINED SERVICES
 * -------------------
 * <p/>
 * The following classes providing services are, by default, statically imported to your scriptlet and thus, all their
 * static methods can be used directly in your scriptlet.
 * <p/>
 * 1. The {@link org.openiot.gsn.utils.services.EmailService} class provides access the Email notification services.
 * 2. The {@link org.openiot.gsn.utils.services.TwitterService} class provides access to Twitter notifications services.
 * <p/>
 * LIMITATIONS
 * -----------
 * <p/>
 * 1. The variables names binded into the script are uppercase.
 */
public class ScriptletProcessor extends AbstractVirtualSensor {

    private static final transient Logger logger = Logger.getLogger(ScriptletProcessor.class);

    private static final String PARAM_SCRIPTLET = "scriptlet";

    private static final String PARAM_SCRIPTLETPERIODIC = "scriplet-periodic";

    private static final String PARAM_PERIOD = "period";

    private static final String PARAM_PERSITANT = "persistant";

    private Timer timer = null;

    /**
     * This field holds the scriplet (state and logic) executed upon reception of a new {@link org.openiot.gsn.beans.StreamElement}.
     */
    protected Script scriptlet = null;

    /**
     * This field holds the scriplet (state and logic) executed periodically.
     */
    protected Script scriptletPeriodic = null;

    /**
     * This field holds the context (variable state) which is shared among
     * the {@link org.openiot.gsn.processor.ScriptletProcessor#scriptlet}
     * and {@link org.openiot.gsn.processor.ScriptletProcessor#scriptletPeriodic} fields.
     */
    protected final Binding context = new Binding();

    protected DataField[] outputStructure = null;

    private long period = -1;

    private boolean persistant = true;

    private TimerTask periodicalTask = null;

    @Override
    public boolean initialize() {
        return initialize(
                getVirtualSensorConfiguration().getOutputStructure(),
                getVirtualSensorConfiguration().getMainClassInitialParams()
        );
    }

    @Override
    public void dispose() {
        if (periodicalTask != null)
            periodicalTask.cancel();
    }

    @Override
    public void dataAvailable(String inputStreamName, StreamElement se) {
        evaluate(scriptlet, se, persistant);
    }

    protected boolean initialize(DataField[] outputStructure, TreeMap<String, String> parameters) {
        if (outputStructure == null) {
            logger.warn("Failed to initialize the processing class because the outputStructure is null.");
            return false;
        } else
            this.outputStructure = outputStructure;

        // Mandatory Parameters

        String p = parameters.get(PARAM_PERIOD);
        if (p != null) {
            try {
                period = Long.parseLong(p);
            }
            catch (Exception e) {
                // ...   
            }
        }

        String ps1 = parameters.get(PARAM_SCRIPTLET);
        if (ps1 != null) {
            scriptlet = initScriptlet(ps1);
            if (scriptlet == null)
                return false;
        }

        String ps2 = parameters.get(PARAM_SCRIPTLETPERIODIC);
        if (ps2 != null) {
            scriptletPeriodic = initScriptlet(ps2);
            if (scriptletPeriodic == null)
                return false;
        }

        // At least one of the following is mandatory: {scriptlet,  scriptlet-periodic}.
        if (scriptlet == null && scriptletPeriodic == null) {
            logger.warn("The Initial Parameter >" + PARAM_SCRIPTLET + "< or >" + PARAM_SCRIPTLETPERIODIC + "< MUST be provided in the configuration file for the processing class.");
            return false;
        }
        if ((scriptletPeriodic != null && period < 0) || (scriptletPeriodic == null && period >= 0)) {
            logger.warn("The Initial Parameters >" + PARAM_SCRIPTLETPERIODIC + "< and >" + PARAM_PERIOD + "< MUST be provided together in the configuration file for the processing class.");
            return false;
        }

        // Optional Parameters

        if (parameters.containsKey(PARAM_PERSITANT)) {
            try {
                persistant = Boolean.parseBoolean(parameters.get(PARAM_PERSITANT));
            }
            catch (Exception e) {
                logger.debug(e.getMessage(), e);
            }
        }

        // Add the periodical task to the timer if needed.
        if (scriptletPeriodic != null && period >= 0) {
            periodicalTask = new TimerTask() {
                public void run() {
                    evaluate(scriptletPeriodic, null, false);
                }
            };
            getTimer().schedule(periodicalTask, 0, period);
        }
        return true;
    }

    protected Script initScriptlet(String ps) {
        StringBuilder scriptlet = new StringBuilder();
        scriptlet.append("// start auto generated part --\n");
        // Add the static import (for predefined services)
        scriptlet.append("import static ").append(org.openiot.gsn.utils.services.EmailService.class.getCanonicalName()).append(".*;\n");
        scriptlet.append("import static ").append(org.openiot.gsn.utils.services.TwitterService.class.getCanonicalName()).append(".*;\n");
        // Add the syntactic sugars
        scriptlet.append("def isdef(var){(binding.getVariables().containsKey(var))}\n");
        scriptlet.append("// end auto generated part --\n");
        // Append the scriplet from the parameter
        scriptlet.append(ps);
        //
        GroovyShell shell = new GroovyShell();
        Script script = null;
        try {
            script = shell.parse(scriptlet.toString());
            logger.debug("Compiled script: \n" + scriptlet.toString());
        }
        catch (Exception e) {
            logger.error("Failed to compile the scriptlet " + e.getMessage());
            return null;
        }
        return script;
    }

    protected StreamElement formatOutputStreamElement(Binding binding) {
        Serializable[] data = new Serializable[outputStructure.length];
        for (int i = 0; i < outputStructure.length; i++) {
            DataField df = outputStructure[i];
            Object o = null;
            try {
                o = binding.getVariable(df.getName().toUpperCase());
            }
            catch (MissingPropertyException e) {
                // ...   
            }
            data[i] = (Serializable) o;
        }
        StreamElement seo = new StreamElement(outputStructure, data);
        try {
            Long timed = (Long) binding.getVariable("TIMED");
            seo.setTimeStamp(timed);
        }
        catch (MissingPropertyException e) {
            // ...
        }
        return seo;
    }

    protected Binding updateContext(StreamElement se) {
        if (se != null) {
            for (String fieldName : se.getFieldNames()) {
                context.setVariable(fieldName.toUpperCase(), se.getData(fieldName));
            }
            context.setVariable("TIMED", se.getTimeStamp());
        }
        return context;
    }

    protected void evaluate(Script script, StreamElement se, boolean persist) {
        StreamElement seo = null;
        synchronized (context) {
            updateContext(se);
            script.setBinding(context);
            script.run();
            if (persist) {
                seo = formatOutputStreamElement(context);
            }
        }
        if (seo != null) {
            dataProduced(seo);
        }
    }

    private synchronized Timer getTimer() {
        if (timer == null)
            timer = new Timer(false);
        return timer;
    }
}
